// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: jshin

#pragma once

#include <string>

#include <rapidjson/document.h>
#include <rapidjson/rapidjson.h>

#include "coin/proto/coin_market_enums.pb.h"
#include "coin2/base/conversion.h"
#include "coin2/exchange/base/feed/update.h"
#include "coin2/exchange/base/feed_util/book_builder.h"
#include "coin2/exchange/base/feed_util/trade.h"

namespace impl {
namespace {

const int kSnapshotLevelMax = 20;
}  // namespace

using namespace std::literals;
using coin2::exchange::base::market::ExchangeType;
using coin2::exchange::base::market::MarketType;

class GdaxFeedUpdateParser {
 public:
  static void ParseBook(
      BookBuilder* book_builder_,
      const rapidjson::Document& book_json,
      bool is_bbo) {
    auto ts = TimestampFromIso8601(book_json["time"].GetString());
    auto tx = book_builder_->NewTransaction(ts);
    if (is_bbo) {
      tx.BboClear();
    }

    for (const auto& change : book_json["changes"].GetArray()) {
      const auto& side = std::string(change[0].GetString());
      if (side == "sell") {
        // Gdax allow big ask price, for instance ["9999999999.00","0.22000000"].
        // We must filter out those big price cases because in fastfeed price
        // will be multiplied by 1e9 and thus may overflow.
        double price = stod(change[1].GetString());
        if (price > 9223372036) {
          continue;
        }
        tx.UpdateAsk(price, stod(change[2].GetString()));
      } else if (side == "buy") {
        tx.UpdateBid(stod(change[1].GetString()), stod(change[2].GetString()));
      }
    }
  }

  static void
  ParseSnapshot(BookBuilder* book_builder_, const rapidjson::Document& book_json, int64_t ts) {
    auto tx = book_builder_->NewTransaction(ts);
    if (book_json["type"].GetString() == "snapshot"sv) {
      tx.Clear();
    }

    int i = 0;
    for (const auto& ask : book_json["asks"].GetArray()) {
      tx.UpdateAsk(stod(ask[0].GetString()), stod(ask[1].GetString()));
      ++i;
      if (i >= kSnapshotLevelMax) {
        break;
      }
    }
    i = 0;
    for (const auto& bid : book_json["bids"].GetArray()) {
      tx.UpdateBid(stod(bid[0].GetString()), stod(bid[1].GetString()));
      ++i;
      if (i >= kSnapshotLevelMax) {
        break;
      }
    }
  }

  static void ParseTrade(const rapidjson::Value& trade_json, Trade* trade) {
    trade->price = stod(trade_json["price"].GetString());
    trade->fill_qty = stod(trade_json["size"].GetString());
    trade->side = ParseTradeSide(trade_json["side"].GetString());
    trade->timestamp = TimestampFromIso8601(trade_json["time"].GetString());
  }

  static coin2::exchange::feed::TradeSide ParseTradeSide(const std::string& side) {
    // If the side is sell this indicates the maker was a sell order
    // and the match is considered an up-tick. A buy side match is a down-tick.
    // maker side
    if (side == "buy") {
      return coin2::exchange::feed::TRADE_SELL_SIDE;
    } else if (side == "sell") {
      return coin2::exchange::feed::TRADE_BUY_SIDE;
    } else {
      throw std::runtime_error("[Gdax] unknown side: " + side);
    }
  }

  static bool ParseKline(
      const rapidjson::Value& kline_json,
      Klines* klines,
      fastfeed::proto::KlineInterval interval,
      int64_t timestamp) {
    const auto& v = kline_json.GetArray();
    CHECK_EQ(v.Size(), 6);
    int64_t open_timestamp = stoll(v[0].GetString()) * 1'000'000'000LL;
    auto result = klines->AddKline(open_timestamp);
    if (!result) return false;

    Kline* kline;
    bool freezed;
    std::tie(kline, freezed) = result.value();
    kline->open_timestamp = open_timestamp;
    kline->low = stod(v[1].GetString());
    kline->high = stod(v[2].GetString());
    kline->open = stod(v[3].GetString());
    kline->close = stod(v[4].GetString());
    kline->volume = stod(v[5].GetString());
    kline->timestamp = timestamp;
    kline->interval = interval;

    return freezed;
  }

  static fastfeed::proto::KlineInterval ParseKlineInterval(std::string_view interval) {
    static std::map<std::string, fastfeed::proto::KlineInterval> interval_map = {
      {"60", fastfeed::proto::KLINE_INTERVAL_1MIN},
      {"300", fastfeed::proto::KLINE_INTERVAL_5MIN},
      {"900", fastfeed::proto::KLINE_INTERVAL_15MIN},
      {"3600", fastfeed::proto::KLINE_INTERVAL_1HOUR},
      {"86400", fastfeed::proto::KLINE_INTERVAL_1DAY}
    };

    auto iter = interval_map.find(std::string(interval));
    if (iter == interval_map.end()) {
      return fastfeed::proto::KLINE_INTERVAL_INVALID;
    }

    return iter->second;
  }
};
}  // namespace impl

using impl::GdaxFeedUpdateParser;
